Remove "locals" from Drop classes and introduce a real context class.

The Context class is capable of switching a self object and adding local
variables, and also hashable for the convenience of being used as a
cache key.

As for the thread safety, we give it up for now as we don't support it
or have concurrency tests yet anyway.

Akinori MUSHA 10 years ago
parent
commit
e2f2ff5da8

+ 7 - 39
app/concerns/liquid_droppable.rb

@@ -1,48 +1,16 @@
1
+# Include this mix-in to make a class droppable to Liquid, and adjust
2
+# its behavior in Liquid by implementing its dedicated Drop class
3
+# named with a "Drop" suffix.
1 4
 module LiquidDroppable
2 5
   extend ActiveSupport::Concern
3 6
 
4
-  # In subclasses of this base class, "locals" take precedence over
5
-  # methods.
6 7
   class Drop < Liquid::Drop
7
-    class << self
8
-      def inherited(subclass)
9
-        class << subclass
10
-          attr_reader :drop_methods
11
-
12
-          # Make all public methods private so that #before_method
13
-          # catches everything.
14
-          def drop_methods!
15
-            return if @drop_methods
16
-
17
-            @drop_methods = Set.new
18
-
19
-            (public_instance_methods - Drop.public_instance_methods).each { |name|
20
-              @drop_methods << name.to_s
21
-              private name
22
-            }
23
-          end
24
-        end
25
-      end
26
-    end
27
-
28
-    def initialize(object, locals = nil)
29
-      self.class.drop_methods!
30
-
8
+    def initialize(object)
31 9
       @object = object
32
-      @locals = locals || {}
33
-    end
34
-
35
-    def before_method(name)
36
-      if @locals.include?(name)
37
-        @locals[name]
38
-      elsif self.class.drop_methods.include?(name)
39
-        __send__(name)
40
-      end
41 10
     end
42 11
 
43 12
     def each
44
-      return to_enum(__method__) unless block_given?
45
-      self.class.drop_methods.each { |name|
13
+      (public_instance_methods - Drop.public_instance_methods).each { |name|
46 14
         yield [name, __send__(name)]
47 15
       }
48 16
     end
@@ -52,7 +20,7 @@ module LiquidDroppable
52 20
     const_set :Drop, Kernel.const_set("#{name}Drop", Class.new(Drop))
53 21
   end
54 22
 
55
-  def to_liquid(*args)
56
-    self.class::Drop.new(self, *args)
23
+  def to_liquid
24
+    self.class::Drop.new(self)
57 25
   end
58 26
 end

+ 78 - 12
app/concerns/liquid_interpolatable.rb

@@ -18,28 +18,94 @@ module LiquidInterpolatable
18 18
     false
19 19
   end
20 20
 
21
-  def interpolate_options(options, event = {})
22
-    case options
21
+  # Return the current interpolation context.  Use this in your Agent
22
+  # class to manipulate interpolation context for user.
23
+  #
24
+  # For example, to provide local variables:
25
+  #
26
+  #     # Create a new scope to define variables in:
27
+  #     interpolation_context.stack {
28
+  #       interpolation_context['_something_'] = 42
29
+  #       # And user can say "{{_something_}}" in their options.
30
+  #       value = interpolated['some_key']
31
+  #     }
32
+  #
33
+  def interpolation_context
34
+    @interpolation_context ||= Context.new(self)
35
+  end
36
+
37
+  # Take the given object as "self" in the current interpolation
38
+  # context while running a given block.
39
+  #
40
+  # The most typical use case for this is to evaluate options for each
41
+  # received event like this:
42
+  #
43
+  #     def receive(incoming_events)
44
+  #       incoming_events.each do |event|
45
+  #         interpolate_with(event) do
46
+  #           # Handle each event based on "interpolated" options.
47
+  #         end
48
+  #       end
49
+  #     end
50
+  def interpolate_with(self_object)
51
+    case self_object
52
+    when nil
53
+      yield
54
+    else
55
+      context = interpolation_context
56
+      begin
57
+        context.environments.unshift(self_object.to_liquid)
58
+        yield
59
+      ensure
60
+        context.environments.shift
61
+      end
62
+    end
63
+  end
64
+
65
+  def interpolate_options(options, self_object = nil)
66
+    interpolate_with(self_object) do
67
+      case options
23 68
       when String
24
-        interpolate_string(options, event)
69
+        interpolate_string(options)
25 70
       when ActiveSupport::HashWithIndifferentAccess, Hash
26
-        options.inject(ActiveSupport::HashWithIndifferentAccess.new) { |memo, (key, value)| memo[key] = interpolate_options(value, event); memo }
71
+        options.each_with_object(ActiveSupport::HashWithIndifferentAccess.new) { |(key, value), memo|
72
+          memo[key] = interpolate_options(value)
73
+        }
27 74
       when Array
28
-        options.map { |value| interpolate_options(value, event) }
75
+        options.map { |value| interpolate_options(value) }
29 76
       else
30 77
         options
78
+      end
79
+    end
80
+  end
81
+
82
+  def interpolated(self_object = nil)
83
+    interpolate_with(self_object) do
84
+      (@interpolated_cache ||= {})[[options, interpolation_context]] ||=
85
+        interpolate_options(options)
31 86
     end
32 87
   end
33 88
 
34
-  def interpolated(event = {})
35
-    key = [options, event]
36
-    @interpolated_cache ||= {}
37
-    @interpolated_cache[key] ||= interpolate_options(options, event)
38
-    @interpolated_cache[key]
89
+  def interpolate_string(string, self_object = nil)
90
+    interpolate_with(self_object) do
91
+      Liquid::Template.parse(string).render!(interpolation_context)
92
+    end
39 93
   end
40 94
 
41
-  def interpolate_string(string, event)
42
-    Liquid::Template.parse(string).render!(event.to_liquid, registers: {agent: self})
95
+  class Context < Liquid::Context
96
+    def initialize(agent)
97
+      super({}, {}, { agent: agent }, true)
98
+    end
99
+
100
+    def hash
101
+      [@environments, @scopes, @registers].hash
102
+    end
103
+
104
+    def eql?(other)
105
+      other.environments == @environments &&
106
+        other.scopes == @scopes &&
107
+        other.registers == @registers
108
+    end
43 109
   end
44 110
 
45 111
   require 'uri'

+ 6 - 5
app/models/agents/event_formatting_agent.rb

@@ -120,11 +120,12 @@ module Agents
120 120
 
121 121
     def receive(incoming_events)
122 122
       incoming_events.each do |event|
123
-        payload = perform_matching(event.payload)
124
-        opts = interpolated(event.to_liquid(payload))
125
-        formatted_event = opts['mode'].to_s == "merge" ? event.payload.dup : {}
126
-        formatted_event.merge! opts['instructions']
127
-        create_event :payload => formatted_event
123
+        interpolate_with(event) do
124
+          interpolation_context.merge(perform_matching(event.payload))
125
+          formatted_event = interpolated['mode'].to_s == "merge" ? event.payload.dup : {}
126
+          formatted_event.merge! interpolated['instructions']
127
+          create_event :payload => formatted_event
128
+        end
128 129
       end
129 130
     end
130 131
 

+ 4 - 9
app/models/agents/website_agent.rb

@@ -205,16 +205,11 @@ module Agents
205 205
 
206 206
     def receive(incoming_events)
207 207
       incoming_events.each do |event|
208
-        Thread.current[:current_event] = event
209
-        url_to_scrape = event.payload['url']
210
-        check_url(url_to_scrape) if url_to_scrape =~ /^https?:\/\//i
208
+        interpolate_with(event) do
209
+          url_to_scrape = event.payload['url']
210
+          check_url(url_to_scrape) if url_to_scrape =~ /^https?:\/\//i
211
+        end
211 212
       end
212
-    ensure
213
-      Thread.current[:current_event] = nil
214
-    end
215
-
216
-    def interpolated(event = Thread.current[:current_event])
217
-      super
218 213
     end
219 214
 
220 215
     private

+ 13 - 6
app/models/event.rb

@@ -48,21 +48,28 @@ class Event < ActiveRecord::Base
48 48
 end
49 49
 
50 50
 class EventDrop
51
-  def initialize(object, locals = nil)
52
-    locals = object.payload.merge(locals || {})
51
+  def initialize(object)
52
+    @payload = object.payload
53 53
     super
54 54
   end
55 55
 
56
+  def before_method(key)
57
+    @payload[key]
58
+  end
59
+
56 60
   def each(&block)
57
-    return to_enum(__method__) unless block
58
-    @locals.each(&block)
61
+    @payload.each(&block)
59 62
   end
60 63
 
61 64
   def agent
62
-    @object.agent
65
+    @payload.fetch(__method__) {
66
+      @object.agent
67
+    }
63 68
   end
64 69
 
65 70
   def created_at
66
-    @object.created_at
71
+    @payload.fetch(__method__) {
72
+      @object.created_at
73
+    }
67 74
   end
68 75
 end